<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <title>鼠标粒子特效</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />

    <style>
      * {
        box-sizing: border-box;
      }

      html,
      body {
        height: 100%;
      }

      body {
        overflow: hidden;
        display: grid;
        color: white;
        background: black;
      }
    </style>
  </head>
  <body>
    <pointer-particles></pointer-particles>

    <script>
      class PointerParticle {
        constructor(spread, speed, component) {
          const { ctx, pointer, hue } = component;

          this.ctx = ctx;
          this.x = pointer.x;
          this.y = pointer.y;
          this.mx = pointer.mx * 0.1;
          this.my = pointer.my * 0.1;
          this.size = Math.random() + 1;
          this.decay = 0.01;
          this.speed = speed * 0.08;
          this.spread = spread * this.speed;
          this.spreadX = (Math.random() - 0.5) * this.spread - this.mx;
          this.spreadY = (Math.random() - 0.5) * this.spread - this.my;
          this.color = `hsl(${hue}deg 90% 60%)`;
        }

        draw() {
          this.ctx.fillStyle = this.color;
          this.ctx.beginPath();
          this.ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
          this.ctx.fill();
        }

        collapse() {
          this.size -= this.decay;
        }

        trail() {
          this.x += this.spreadX * this.size;
          this.y += this.spreadY * this.size;
        }

        update() {
          this.draw();
          this.trail();
          this.collapse();
        }
      }

      class PointerParticles extends HTMLElement {
        static register(tag = 'pointer-particles') {
          if ('customElements' in window) {
            customElements.define(tag, this);
          }
        }

        static css = `
    :host {
      display: grid;
      width: 100%;
      height: 100%;
      pointer-events: none;
    }
  `;

        constructor() {
          super();

          this.canvas;
          this.ctx;
          this.fps = 60;
          this.msPerFrame = 1000 / this.fps;
          this.timePrevious;
          this.particles = [];
          this.pointer = {
            x: 0,
            y: 0,
            mx: 0,
            my: 0,
          };
          this.hue = 0;
        }

        connectedCallback() {
          const canvas = document.createElement('canvas');
          const sheet = new CSSStyleSheet();

          this.shadowroot = this.attachShadow({ mode: 'open' });

          sheet.replaceSync(PointerParticles.css);
          this.shadowroot.adoptedStyleSheets = [sheet];

          this.shadowroot.append(canvas);

          this.canvas = this.shadowroot.querySelector('canvas');
          this.ctx = this.canvas.getContext('2d');
          this.setCanvasDimensions();
          this.setupEvents();
          this.timePrevious = performance.now();
          this.animateParticles();
        }

        createParticles(event, { count, speed, spread }) {
          this.setPointerValues(event);

          for (let i = 0; i < count; i++) {
            this.particles.push(new PointerParticle(spread, speed, this));
          }
        }

        setPointerValues(event) {
          this.pointer.x = event.x - this.offsetLeft;
          this.pointer.y = event.y - this.offsetTop;
          this.pointer.mx = event.movementX;
          this.pointer.my = event.movementY;
        }

        setupEvents() {
          const parent = this.parentNode;

          parent.addEventListener('click', (event) => {
            this.createParticles(event, {
              count: 300,
              speed: Math.random() + 1,
              spread: Math.random() + 50,
            });
          });

          parent.addEventListener('pointermove', (event) => {
            this.createParticles(event, {
              count: 20,
              speed: this.getPointerVelocity(event),
              spread: 1,
            });
          });

          window.addEventListener('resize', () => this.setCanvasDimensions());
        }

        getPointerVelocity(event) {
          const a = event.movementX;
          const b = event.movementY;
          const c = Math.floor(Math.sqrt(a * a + b * b));

          return c;
        }

        handleParticles() {
          for (let i = 0; i < this.particles.length; i++) {
            this.particles[i].update();

            if (this.particles[i].size <= 0.1) {
              this.particles.splice(i, 1);
              i--;
            }
          }
        }

        setCanvasDimensions() {
          const rect = this.parentNode.getBoundingClientRect();

          this.canvas.width = rect.width;
          this.canvas.height = rect.height;
        }

        animateParticles() {
          requestAnimationFrame(() => this.animateParticles());

          const timeNow = performance.now();
          const timePassed = timeNow - this.timePrevious;

          if (timePassed < this.msPerFrame) return;

          const excessTime = timePassed % this.msPerFrame;

          this.timePrevious = timeNow - excessTime;

          this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
          this.hue = this.hue > 360 ? 0 : (this.hue += 3);

          this.handleParticles();
        }
      }

      PointerParticles.register();
    </script>
  </body>
</html>
